package com.bitsfromspace.ubertorrent.bencoding;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.bitsfromspace.ubertorrent.bencoding.BValue.Type;


/**
 * BDecoder decodes bencoded messages. To initialize the decoder call {@link #decode(InputStream)} or {@link #decode(String)}.
 * An {@link Iterable} of type {@link BValue} is returned. Any {@link IOException} are wrapped in {@link RuntimeException}.
 * <br/><br/>
 * Example usage:<br/><br/>
  <pre>

Iterable<BValue> bValues = BDecoder.decode(myInputStream);
for (BValue value : bValues){
	//... do something
}

 </pre>
 * @author: chris.oudeman
 * @since: Jan 4, 2011
 */
public class BDecoder {



	private final InputStream in;

	protected BDecoder(InputStream in) {

		this.in = in;
	}

	/**
	 * Returns a Streaming encoder as an {@link Iterable} of type {@link BValue}.
	 */
	public static Iterable<BValue> decode(String in) {

		return decode(new ByteArrayInputStream(in.getBytes()));
	}

	/**
	 * Returns a Streaming encoder as an {@link Iterable} of type {@link BValue}.
	 * <br/>
	 * Any {@link IOException} or {@link EOFException} are wrapped in {@link RuntimeException}.
	 */
	public static Iterable<BValue> decode(final InputStream in) {

		return new Iterable<BValue>() {

			@Override
			public Iterator<BValue> iterator() {

				return createIterator(in);

			}
		};
	}

	/**
	 * Wraps an {@link Iterator} around this {@link BDecoder}.
	 */
	protected static Iterator<BValue> createIterator(InputStream in) {

		final BDecoder decoder = new BDecoder(in);

		return new Iterator<BValue>() {

			private BValue next;

			@Override
			public void remove() {

				throw new UnsupportedOperationException();
			}

			@Override
			public BValue next() {

				if (next == null) {
					next = read();
				}
				BValue value = next;
				next = null;
				return value;

			}

			@Override
			public boolean hasNext() {

				if (next == null) {
					next = read();
				}

				return next != null;
			}

			private BValue read() {

				try {
					return decoder.nextValue();
				} catch (IOException ioEx) {
					throw new RuntimeException(ioEx);
				}
			}
		};

	}

	/**
	 * Reads the next value from the input stream.
	 */
	protected BValue nextValue() throws IOException {

		int nxByte = in.read();
		switch (nxByte) {
		case 'e':
		case -1:
			return null;
		case 'i':
			return readInteger();
		case 'l':
			return readList();
		case 'd':
			return readDictionary();
		default:
			return readByteString(nxByte);
		}
	}

	/**
	 * Reads a {@link Type#INTEGER} from the input stream.
	 */
	private BValue readInteger() throws IOException {

		long integer = readBase10(in.read(), 'e');
		return new BValue(Long.valueOf(integer));

	}

	/**
	 * Reads a {@link Type#STRING} from the input stream.
	 */
	private BValue readByteString(int firstDigit) throws IOException {

		int length = (int) readBase10(firstDigit, ':');

		byte[] stringData = new byte[length];
		int offset = 0;
		int actualRead = -1;
		while ((actualRead =
				in.read(stringData, offset, stringData.length - offset)) != -1) {

			if (offset == stringData.length) {
				break;
			}

			offset += actualRead;
		}

		return new BValue(stringData);

	}

	/**
	 * Reads ASCII base 10 starting with <em>firstDigit</em> until it encounters <em>readUntilChar</em>
	 */
	private long readBase10(int firstDigit, int readUntilChar)
																throws IOException {

		if (firstDigit == -1) {
			throw new EOFException("EOF reached.");
		}

		long length = convertBase10(firstDigit);
		int nxByte = in.read();
		while (nxByte != readUntilChar && nxByte != -1) {
			length = (length * 10) + convertBase10(nxByte);
			nxByte = in.read();
		}
		// nxByte is either ':' or -1
		if (nxByte == -1) {
			throw new EOFException("EOF reached.");
		}
		return length;
	}

	/**
	 * Converts an ASCII base 10 encoded character to a Java integer.
	 */
	private int convertBase10(int digit) {

		if (digit < 0x30 || digit > 0x39) {
			throw new IllegalArgumentException("Invalid base 10 ASCII: "
					+ digit + "(" + ((char) digit) + ")");
		}
		return digit - 0x30;
	}

	/**
	 * Reads a {@link Type#MAP} from the input stream.
	 */
	private BValue readDictionary() throws IOException {

		// LinkedMap preserves the order of the elements inserted into the map
		Map<BValue, BValue> map = new LinkedHashMap<BValue, BValue>();
		BValue key = null;
		//loop through keys & values...
		while ((key = nextValue()) != null) {
			map.put(key, nextValue());
		}
		return new BValue(map);
	}

	/**
	 * Reads a {@link Type#LIST} from the input stream.
	 */
	private BValue readList() throws IOException {

		List<BValue> list = new ArrayList<BValue>();

		//loop through values until either there is no more data or 'e' is encountered.
		BValue value = null;
		while ((value = nextValue()) != null) {
			list.add(value);
		}

		// put the list inside a bvalue...
		return new BValue(list);
	}



}


